Skip to content

fix(shader): LDS float atomics quiet signalling NaNs and flush denormals as the hardware does - #2312

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
SleglJan:feat/shader-lds-float-atomics
Oct 10, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
SleglJan:feat/shader-lds-float-atomics

Conversation

@SleglJan

@SleglJan SleglJan commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Follow-up to #573 (the 64-bit LDS atomics), whose TechnicalDebt entry said the f64 forms were not measured on hardware. ds_min/max_f32, ds_cmpst_f32, ds_min/max_f64, ds_cmpst_f64 and their _rtn forms now follow the rule measured on RDNA2 (gfx1036, Ryzen 7950X3D iGPU) with tools/hw-oracle, over 14 × 14 operand sets per type (±0, ±1, ±inf, ±smallest denormal, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field (fields 1 and 2 added after review); every memory and returned result of the six f64 and nine f32 results per pair, in each of the eight modes, 0 mismatches:

  • both operands are flushed to a signed zero when bit 0 of the type's denormal field is clear (values 0 and 2; 1 and 3 keep them; FLOAT_MODE bits 5:4 for f32, bits 7:6 for f64; a shader without a known mode flushes f32 and keeps f64, as elsewhere);
  • a signalling-NaN data operand is stored quieted; else a signalling-NaN old value is stored quieted; else a quiet-NaN old value is replaced by the data; else a quiet-NaN data operand loses; else the ordered compare with −0 below +0 decides; the flushed value is what is written, also when the old value wins;
  • cmpst compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ;
  • the returned value is always the old value as it was in memory; IEEE_MODE has no effect.

This is the rule AtomicFloatBits::minMax already implements for the buffer float atomics, plus the flush. The shared f32 path used OpIsNan/OpFOrdLessThan/OpFOrdEqual on the bit-cast values, so its NaN and denormal results were the driver's (both RADV and NVIDIA quiet nothing and flush f32 denormals inside those compares in every mode); the shared f64 path (#573) ordered the bits but had none of the signalling-NaN rules. Both now use AtomicFloatBits; the translation marks the shared float atomics with the mode's flush in MemoryInfo::flushDenormals. The 29 integer 64-bit forms, EXEC masking, immediate offsets, out-of-range addresses (0, no write) and contention (256 lanes, serial order) were measured as well and match the recompiler already; ds_add_f32 is not measured and is unchanged.

TechnicalDebt: the 64-bit entry loses its "not measured" sentence and gains the measured facts about mixed-width serialisation (hardware serialises 32-bit and 64-bit atomics on one address, the lock does not: in the measurement RADV lost 64 of 128 increments in wave32 and 32 in wave64, NVIDIA 48 in wave32, timing-dependent) and the misaligned fault; a new LDS float atomics entry records the rule; the f32 denormals entry points to it.

Measurement data, scripts and the host comparison before and after the change: https://github.com/SleglJan/AnyPS5/tree/audit/oracle-2026-10-08/oracle-audit/lds64 (report.md there is the summary).

Overlap: #2162 adds a sentence to the same f32 denormals TechnicalDebt entry (its v_fma_mixlo_f16/v_fma_mixhi_f16 flush rule), and #2025 and #1290 add entries next to the edited ones; none of them shares code with this PR or is built on by it, so whichever merges second rebases the one line. No open pull request changes the shared float atomics.

Tested

Linux (CachyOS, kernel 7.2.9, GCC 16.2.1), Release, ANYPS5_ENABLE_SPIRV_TOOLS=ON, RTX 3080 (615.71.09) and RADV (Mesa 26.2.4), on this branch (based on main d70b899).

  • New agc_driver_lds_float_atomic_modes: 32 operand pairs per type (the classes above, both orders), each lane runs the six f32 and six f64 forms on its LDS slot; 27 result dwords per lane pinned bit for bit against the oracle in the titles' mode (0xc0, also used for "no float mode"), with both fields kept (0xf0), both flushed (0x00), both at 1 (0x50, kept) and both at 2 (0xa0, flushed). Written first: it failed on main's lowering at lane 7 (ds_min_rtn_f32, flushed denormal) and passes with the change.
  • agc_driver_lds_atomics64: its CPU model gets the signalling-NaN rule, and the lanes of kind 2 now take their data from floats[(tid / 4 + 9) % 12], which feeds the table's signalling NaN, −inf and the negative denormal into min/max (the old index never reached entry 9). It failed on main at lane 2 (ds_min_f64, the signalling-NaN data not stored quieted) and passes with the change.
  • Mutation checks, each fails: the 64-bit path without the signalling-NaN rules (both tests), the flush disabled (new test, lane 7), the old signalling NaN preferred over the data (new test, lane 22).
  • The measured matrices replayed through the recompiler on both GPUs with the change: 0 of 35,072 result dwords differ from the oracle over the 256 lanes (f64 min/max 3,072 × 4 modes, f64 cmpst 1,536 × 4, f32 min/max/cmpst 2,304 × 4, 29 integer forms 7,424); before the change, per mode, 472 (f64 min/max, kept), 584 (f64 min/max, flushed), 40 (f64 cmpst, flushed), 336 (f32, kept) and 412 (f32, flushed) dwords differed, the same on both GPUs.
  • agc_driver_lds_atomics, agc_driver_lds_integer_atomics, agc_driver_lds_condxchg, agc_driver_lds_read_write, agc_driver_lds_src2 pass on both GPUs; LDS tests --repeat until-fail:3 on both; full ctest 516/517 on each GPU; the one failure differs per GPU and runs no LDS instruction: agc_driver_storage_cache_budget on the iGPU (its memory budget) and agc_driver_dcc_keys_first_write on the RTX 3080 ("an unchanged pending image was uploaded again", also reported in several open pull requests today).
  • tools/check_conventions.py --base upstream/main, tools/check_test_registration.py pass.
  • Head 59e1899 (review follow-up: flush for field 2 as well, the 0x50 and 0xa0 modes): agc_driver_lds_float_atomic_modes and agc_driver_lds_atomics64 pass three repeats on RADV and the RTX 3080; the new 0xa0 mode fails without the change (lane 7, ds_max_rtn_f32); conventions and test registration pass. Fork Build on head 59e1899 merged into main a291ae0, run 38030543364: relinker green on all three platforms, linux (lavapipe) 539 passed, 29 skipped, 1 failed of 569 with both LDS tests passing (the failure is guest_video_out_resolution_status aborts on the linux CI job since #1714 merged: sceVideoOutOpen creates the window (#1367) and the test has no display guard #2509 on that base, fixed on main since); windows stops at Windows build fails on main: duplicate patched export name: LibwuIonIBw in libSceVideoOut (since #1339) #2462's build break on that base.
  • Windows: the fork's Build workflow on head 3e8d755 merged onto main d70b899, run 37947911451: Windows builds and passes 508/508 with the AGC execution tests skipped (no Vulkan device on the runner, as for every execution test); the Linux job runs the new test on lavapipe (3.3 s) and passes 517/517; the relinker matrix passes on all three systems..
  • Native Windows (popcoder565, RTX 3060 Laptop, NVIDIA 616.92, head 3e8d755 on main f8c2f07): agc_driver_lds_atomics64 and agc_driver_lds_float_atomic_modes pass; full suite 512 of 513, the one failure an intermittent test from fix(agc): storage access to a depth surface's stencil plane through its stencil aspect #2325.

Checklist

🤖 Generated with Claude Code

@SleglJan

SleglJan commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

The overlap with #2162 is one TechnicalDebt line: both PRs add a sentence to the f32 denormals entry (this one for the LDS float atomics, #2162 for v_fma_mixlo_f16/v_fma_mixhi_f16). No shared code and no dependency, so whichever merges second rebases that line; noted in the description.

AI-assisted: yes (Claude Code).

@popcoder565

Copy link
Copy Markdown
Contributor

Native Windows run, since no Windows results were posted. Setup: Windows 10 (19045), RTX 3060 Laptop (NVIDIA 616.92), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. Head 3e8d7553, merged onto main f8c2f07 in one build with 9 other PRs that had no Windows run.

No title was run.

🤖 Addressed by Claude Code

@Kikoarl

Kikoarl commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Tested on Linux (CachyOS x86_64, Intel RPL-S iGPU / Mesa 26.3.0-devel / Vulkan 1.4.363, GCC 16.2.1) at 3e8d7553:

  • Build: Built cleanly in RAM (/tmp/anyps5_build) with zero warnings/errors.
  • Conventions: python3 tools/check_conventions.py --base origin/main --head HEAD -> PASS (0 errors).
  • Verification: Verified LdsAtomics64.cpp and LdsFloatAtomicModes.cpp tests. Accurately quiets signalling NaNs and respects denormal flushing modes in shared memory atomic operations following RDNA hardware behavior.

LGTM!

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measured on RDNA2 (gfx1035) with tools/hw-oracle: ds_min/max/cmpst(_rtn)_f32 and _f64 over 15 × 15 operand pairs (±0, ±1, 2, ±inf, ±smallest and largest denormal, quiet/signalling NaNs with payloads), all nine memory/returned results per pair, IEEE 0 and 1, and every value 0-3 of the type's denormal field (f32 field for the f32 forms, f16/f64 field for the f64 forms). Your rule matches the hardware bit for bit for fields 0, 1 and 3 in both IEEE modes. Locally the build, the LDS tests (RADV and CI's lavapipe, 7.2 s / 3.4 s) and the negative checks (both tests fail without the change) pass.

One thing to change:

  • SharedMemoryInstructions.cpp:220 and :224 flush only for field 0, and TechnicalDebt (TechnicalDebt.md:366) says fields 1 and 2 are "treated as keeping denormals". Field 2 (FP_DENORM_FLUSH_IN) flushes both operands on the hardware, exactly like field 0: 228 of 2025 results per mode differ from this PR, the same in IEEE 0 and 1 and for f32 and f64, e.g.

    • ds_max_f32, old 0x00000000, data 0x00000001: hardware 0x00000000, PR 0x00000001
    • ds_min_f32, old 0x00000000, data 0x80000001: hardware 0x80000000, PR 0x80000001
    • ds_cmpst_f32, old 0x00000000, compare 0x00000001: hardware stores data1, PR leaves 0x00000000
    • ds_max_f64, old 0, data 0x0000000000000001: hardware 0, PR 1

    Field 1 (FP_DENORM_FLUSH_OUT) keeps them, as the PR does. So the flush is on input: flush when bit 0 of the field is clear ((field & 1u) == 0u). A test row with field 2 (and one with field 1) for both types would pin it, and the TechnicalDebt sentence can then drop "denormal field values 1 and 2" from "Not measured".

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
@SleglJan

Copy link
Copy Markdown
Contributor Author

Thanks, confirmed here. New commit 59e1899 on top of 3e8d755: the flush decision for the LDS float atomics is now (field & 1u) == 0u for the f32 field (bits 5:4) and the f16/f64 field (bits 7:6); a shader without a known mode still flushes f32 and keeps f64.

Measured on RDNA2 (gfx1036, Ryzen 7950X3D iGPU) with tools/hw-oracle: ds_min/max/cmpst(_rtn)_f32 and _f64, the PR's 14 × 14 operand pairs, every memory and returned result (9 per pair per type, 1,764 per type and mode), IEEE 0 and 1, every value 0-3 of the type's denormal field. Results that differ from each rule:

field IEEE old rule (flush when field == 0), f32 / f64 field bit 0 clear, f32 / f64
0 0, 1 0 / 0 0 / 0
1 0, 1 0 / 0 0 / 0
2 0, 1 132 / 132 0 / 0
3 0, 1 0 / 0 0 / 0

Field 2 gives the same results as field 0 and field 1 the same as field 3 on all 196 pairs, in both IEEE modes and for both types, e.g. ds_max_rtn_f32 old 0x00000000, data 0x00000001: hardware memory 0x00000000 at field 2 (old rule 0x00000001).

Changes:

  • SharedMemoryInstructions.cpp: & 3u → & 1u in both lines of sharedFloatFlush.
  • agc_driver_lds_float_atomic_modes: FLOAT_MODE 0x50 (both fields 1) checked against the kept table and 0xa0 (both fields 2) against the flushed table, the tables the oracle gives for those fields.
  • TechnicalDebt: the entry states that bit 0 of the field clear (0 and 2) flushes both operands and 1 and 3 keep them; "denormal field values 1 and 2" is dropped from "Not measured".

Tested on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1), Release, ANYPS5_ENABLE_SPIRV_TOOLS=ON, head 59e1899:

Data and script (132 is over the PR's 14 × 14 set, not your 15 × 15): https://github.com/SleglJan/AnyPS5/tree/audit/oracle-2026-10-08/oracle-audit/lds-denorm-fields

AI-assisted: yes (Claude Code)

@SleglJan
SleglJan force-pushed the feat/shader-lds-float-atomics branch from 59e1899 to edb53ae Compare October 10, 2026 06:42
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 10, 2026
@SleglJan

Copy link
Copy Markdown
Contributor Author

Rebased onto main 092e133 (heads d6c0df2 and edb53ae). The only conflict was the f32-denormals TechnicalDebt line, which main had edited too (v_div_fixup_f32, v_mad_legacy_f32); it now carries both edits. The code and test patch is byte-identical to the previous head 59e1899.

Linux (CachyOS, kernel 7.2.9, GCC 16.2.1), Release, head edb53ae: agc_driver_lds_float_atomic_modes and agc_driver_lds_atomics64 pass three repeats on RADV and on the RTX 3080; check_conventions.py --base upstream/main and check_test_registration.py pass. Fork Build on this head merged into main 092e133: run 38031846771, all jobs green; linux (lavapipe) 634 of 634 with 33 expected skips, agc_driver_lds_atomics64 6.1 s and agc_driver_lds_float_atomic_modes 4.6 s; windows 642 of 642 (the two LDS tests skip there, no Vulkan driver on the runner).

AI-assisted: yes (Claude Code)

SleglJan and others added 2 commits October 10, 2026 09:40
…als as the hardware does

ds_min/max/cmpst_f32 and ds_min/max/cmpst_f64 with their _rtn forms follow the
rule measured on RDNA2 (gfx1036): both operands are flushed to a signed zero
when the type's denormal field flushes, a signalling-NaN data operand is stored
quieted, else a signalling-NaN old value, else a quiet NaN loses, else the
ordered compare decides, and the flushed value is written. The shared f32
path used the host's float compares, so its NaN and denormal results were the
driver's; the shared f64 path lacked the signalling-NaN rules the buffer path
has. Both now use AtomicFloatBits. The translation marks the shared float
atomics with the mode's flush in MemoryInfo.

The new agc_driver_lds_float_atomic_modes pins 32 operand pairs per type in
the titles' mode, with both fields kept and with both flushed. The 64-bit
test's model gets the signalling-NaN rule and one lane kind now feeds the
signalling NaN of its table into min/max.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GLfKH7Sqe51wX254k1XsV
…well

On RDNA2 (gfx1036) ds_min/max/cmpst(_rtn)_f32 and _f64 flush both operands
when bit 0 of the type's denormal field is clear: field 2 flushes like field
0, field 1 keeps like field 3, in IEEE 0 and 1. The flush decision now tests
that bit. agc_driver_lds_float_atomic_modes adds FLOAT_MODE 0x50 (both fields
1) and 0xa0 (both fields 2), and the TechnicalDebt entry records the measured
fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GLfKH7Sqe51wX254k1XsV
@SleglJan
SleglJan force-pushed the feat/shader-lds-float-atomics branch from edb53ae to 4fb613e Compare October 10, 2026 07:43
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 10, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked on the PR merged onto current main: build and the libSceAgc/libSceAgcDriver ctest pass, including agc_driver_lds_atomics64 and agc_driver_lds_float_atomic_modes; both fail with the non-test changes reverted. The flush rule (field & 1) == 0 for fields 0-3 matches what I measured on gfx1035 at IEEE 0 and 1. The code and tests are unchanged since the head I measured; the new head only rebases the f32 denormals TechnicalDebt line. Merging.

@adriadam10
adriadam10 merged commit 700d5a6 into boykopovar:main Oct 10, 2026
14 checks passed
@SleglJan

Copy link
Copy Markdown
Contributor Author

Rebased again, onto main 4e9d298 (heads c2a9491 and 4fb613e): the same f32-denormals TechnicalDebt line, which main rewrote this morning (tininess after rounding, v_mullit_f32), carries both edits again; the code and test patch is byte-identical to 59e1899.

Linux (CachyOS, kernel 7.2.9, GCC 16.2.1), Release, head 4fb613e: agc_driver_lds_float_atomic_modes and agc_driver_lds_atomics64 pass three repeats on RADV and on the RTX 3080; check_conventions.py --base upstream/main and check_test_registration.py pass. Fork Build on this head merged into main 4e9d298, run 38035426977: relinker green on all three platforms; linux (lavapipe) 640 of 640 with 34 expected skips, the two LDS tests passing (3.3 s and 4.2 s); windows 647 of 648, the LDS tests skipped there (no Vulkan driver), the one failure agc_driver_prepared_shaders_deferred_undecodable, which is not this PR's: the fork's build of main 4e9d298 alone (run 38035425615) fails the same test.

AI-assisted: yes (Claude Code)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants